Python for samfunnsøkonomi

Forelesningsnotater - Espen Sirnes

9 - webskraping med python

HTML-kode

Når vi skal skrape nettsider, analyserer vi "kildekoden" som ligger bak nettsiden. I de fleste nettleser kan du enkelt se kildekoden ved å høyreklikke på siden og velge "view page source" eller lignende. I denne leksjonen skal vi begynne med å skrape rentebarometeret til Norsk Famileøkonomi. For å se hva vi skal skrape kan du derfor gå til https://www.norskfamilie.no/barometre/rentebarometer/, høyreklikke og velge å se kildekoden.

Elementer er markert i en nettside med såkalte "html-tagger". For eksempel lager du kursiv på en nettside ved å skrive <i>kursiv</i>. Denne teksten er skrevet i "markdown", som også forstår html-tagger. Om du leser dette interaktivt i en jupyterfil kan du dobbelklikke her og se at denne setningen er skrevet inne i kursivtagger.

Når vi skraper websider er innholdet vi er interessert i veldig ofte inne i en tabell. Det er det her også. Gjør et tekstsøk i kildekoden etter "\<table". Det finnes kun én plass i dokumentet, og markerer begynnelsen på tabellen. Søker du én gang til med "\</table>" finner du hvor tabellen ender.

I mellom disse taggene er det en god del kode som kanskje ser veldig komplisert ut. Men vi trenger kun å forholde oss til følgende tre typer tagger:

  • <tr>: rad
  • <th>: overskrift
  • <td>: celle

For å hente ut innholdet i tabellen må vi altså søke etter disse taggene, etter at vi har identifisere teksten mellom "tabell"-taggene. Det finnes heldigvis et veldig godt verktøy for dette i python, som heter BeutifulSoup (pip install beautifulsoup4 i kommandovinduet om det ikke er installert).

Med dette verktøyet kan du enkelt finne de taggene du ønsker. Vi starter med å finne selve tabellen, etter å ha bruke pakken requests til å laste ned html-filen:

Eksempel 1:

In [1]:
from bs4 import BeautifulSoup
import requests

def fetch_html_tables(url):
    "Returns a list of tables in the html of url"
    page = requests.get(url)
    bs=BeautifulSoup(page.content)
    tables=bs.find_all('table')
    return tables

tables=fetch_html_tables('https://www.norskfamilie.no/barometre/rentebarometer/')
table_html=tables[0]

#printing top
print(str(table_html)[:1000])
<table class="table table-striped table-hover barometer">
<thead>
<tr>
<th> </th>
<th>Bank</th>
<th> </th>
<th class="d-none d-sm-table-cell">Navn</th>
<th>Nominell</th>
<th class="d-none d-sm-table-cell">Sikkerhets<br/>gebyr</th>
<th class="d-none d-sm-table-cell">Etablerings<br/>gebyr</th>
<th class="d-none d-sm-table-cell">Termin</th>
<th>Effektiv</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Høland og Setskog Sparebank</td>
<td>
<button class="popover_info btn btn-none" data-html="true" data-placement="left" data-toggle="tooltip" title="Nominell: &lt;strong&gt;4,90&lt;/strong&gt;&lt;br&gt;Sikkerhetsgebyr: &lt;strong&gt;0&lt;/strong&gt;&lt;br&gt;Etableringsgebyr: &lt;strong&gt;0&lt;/strong&gt;&lt;br&gt;Termingebyr: &lt;strong&gt;75&lt;/strong&gt;&lt;br&gt;Effektiv rente: &lt;strong&gt;5,11&lt;/strong&gt;&lt;br&gt;&lt;br&gt;Energimerke A eller B, samt kjøp av bolig i Aurskog-Høland." type="button">
<i class="fa fa-info-circle"></i>
</button>
</td>
<td class="d-none d-sm-table-cell"

Få tak i HTML-tabellene

Det vi får ut med bs.find_all('table') er altså en liste med alle partier i teksten med matchende <table>-</table>-tagger. I dette dokumentet er det bare én tabell, så listen har bare ett element. Vi må nå søke videre inne i tabellen etter innholdstaggene. Vi bruker samme funksjon til det. Her er to funksjoner som sammen finner innholdstaggene og returnerer en tabell:

Eksempel 2:

In [2]:
def html_to_table(html):
    "Returns the table defined in html as a list"
    #defining the table:
    table=[]
    #iterating over all rows
    for row in html.find_all('tr'):
        r=[]
        #finding all cells in each row:
        cells=row.find_all('td')
        
        #if no cells are found, look for headings
        if len(cells)==0:
            cells=row.find_all('th')
            
        #iterate over cells:
        for cell in cells:
            cell=format(cell)
            r.append(cell)
        
        #append the row to t:
        table.append(r)
    return table

def format(cell):
    "Returns a string after converting bs4 object cell to clean text"
    if cell.content is None:
        s=cell.text
    elif len(cell.content)==0:
        return ''
    else:
        s=' '.join([str(c) for c in cell.content])
        
    #here you can add additional characters/strings you want to 
    #remove, change punctuations or format the string in other
    #ways:
    s=s.replace('\xa0','')
    s=s.replace('\n','')
    return s

table=html_to_table(table_html)

#printing top
print(str(table)[:1000])
[['', 'Bank', '', 'Navn', 'Nominell', 'Sikkerhetsgebyr', 'Etableringsgebyr', 'Termin', 'Effektiv'], ['1', 'Høland og Setskog Sparebank', '', 'Grønt Boliglån spesial', '4,90', '0', '0', '75', '5,11'], ['2', 'Statens pensjonskasse', '', 'Boliglån inntil 80 %', '4,99', '0', '0', '50', '5,16'], ['3', 'Nordnet Bank (Nordnet Bank AB)', '', 'Boliglån Private Banking (nivå 4)', '5,14', '0', '0', '0', '5,26'], ['4', 'Romsdal Sparebank', '', 'Grønt boliglån', '5,15', '1000', '2500', '60', '5,35'], ['5', 'Oppdalsbanken', '', 'Grønt boliglån innenfor 50%', '5,20', '1200', '0', '55', '5,40'], ['6', 'Ørskog Sparebank', '', 'Grønt bustadlån', '5,20', '1000', '0', '70', '5,41'], ['7', 'Høland og Setskog Sparebank', '', 'Grønt Boliglån', '5,20', '0', '0', '75', '5,42'], ['8', 'Landkreditt Bank AS', '', 'Grønt Boliglån 50%', '5,29', '500', '0', '0', '5,42'], ['9', 'Berg Sparebank', '', 'Grønt Boliglån 75%', '5,24', '1000', '0', '50', '5,43'], ['10', 'Vekselbanken', '', 'Grønt bustadlån', '5,25', '0', '0

Den første funksjonen itererer over tabellceller, mens den andre funksjonen konverterer innholdet fra et bs4-objekt med html-kode til leselig tekst.

Lagre tabellen

Vi har nå skrapet siden, og hentet ut tabellen. For å gjøre den mer leselig, kan vi lagre den som en fil. Når vi lager filer i python bruker vi den innebygde open-funksjonen. Om vi kaller filen for "rentebarometer.csv", kan vi opprette filen ved å kjøre f=open('rentebarometer.csv','w'). Strengen 'w' betyr at vi åpner filen for skriving (writing, i motsetning til lesing/reading markert med 'r'. Vi fyller filen med innhold med f.write().

For å skille kolonnene skal vi her bruke semikolon ';'. Python har en enkel måte å konvertere en liste til en streng med skilletegn. En tar utgangspunkt i skilletegnet, og bruker metoden join() på det. For eksempel:

Eksempel 3:

In [3]:
';'.join(table[0])
Out[3]:
';Bank;;Navn;Nominell;Sikkerhetsgebyr;Etableringsgebyr;Termin;Effektiv'

Vi kan nå åpne filen for skriving og iterere over rader og skrive dem til filen.

Eksempel 4:

In [4]:
def save_data(file_name,table):
    "Saves table to file_name"
    f=open(file_name,'w')
    for row in table:
        f.write(';'.join(row)+'\n')
    f.close()
    
save_data('rentebarometer.csv',table)
In [7]:
import pandas as pd
pd.read_csv('rentebarometer.csv', delimiter=';')
Out[7]:
Unnamed: 0 Bank Unnamed: 2 Navn Nominell Sikkerhetsgebyr Etableringsgebyr Termin Effektiv
0 1 Høland og Setskog Sparebank NaN Grønt Boliglån spesial 4,90 0 0 75 5,11
1 2 Statens pensjonskasse NaN Boliglån inntil 80 % 4,99 0 0 50 5,16
2 3 Nordnet Bank (Nordnet Bank AB) NaN Boliglån Private Banking (nivå 4) 5,14 0 0 0 5,26
3 4 Romsdal Sparebank NaN Grønt boliglån 5,15 1000 2500 60 5,35
4 5 Oppdalsbanken NaN Grønt boliglån innenfor 50% 5,20 1200 0 55 5,40
... ... ... ... ... ... ... ... ... ...
405 406 Kraft Bank ASA NaN Refinansieringslån med sikkerhet 8,00 0 0 75 8,40
406 407 INSTABANK ASA NaN Lån med sikkerhet 8,10 0 5000 50 8,47
407 408 MYBANK ASA NaN Omstartslån mybank 8,50 0 0 0 8,84
408 409 Bluestep Bank AB (publ), filial Oslo NaN Bluestep Boliglån 8,60 0 20000 25 8,98
409 410 Kraft Bank ASA NaN 2. prioritetslån 9,50 0 0 75 10,02

410 rows × 9 columns

In [ ]:
 

Vi kan ta en kikk på dataene med Pandas (på windows må du kanskje ta med encoding='latin1' som argument for å få med æ,ø,å):

Eksempel 5:

En funksjon som skraper alle tabeller på en side

Vi kan sammenfatte stegene over i én funksjon, som anvender

Eksempel 6:

In [5]:
from bs4 import BeautifulSoup
import requests

def scrape(url, file_name):
    table=[]
    tables=fetch_html_tables(url)
    #iterate over all tables, if there are more than one:
    for tbl in tables:
        #exends table so that table is a list containing elements 
        #from all tables:
        table.extend(html_to_table(tbl))
    #saving it:
    save_data(file_name,table)
    return table

Med denne koden kan vi nå skrape hvilken som helst nettside med tabeller vi ønsker å få tak i. For eksempel om vi ønsker å hente timeplanen til kurset:

Eksempel 7:

In [6]:
url=('https://timeplan.uit.no/'
'emne_timeplan.php?sem=24v&fag=&module[]=SOK-1005-1#week-15')
file_name='schedule.csv'

table=scrape(url,file_name)

s='\n'.join(['\t'.join(row) for row in table])


#printing top
print(str(s)[:1000])
Uke  2	Mandag  08.01.2024	Tirsdag  09.01.2024	Onsdag  10.01.2024	Torsdag  11.01.2024	Fredag  12.01.2024
08:00					
09:00					
10:00					
11:00					
12:00					Aktiviteter i tidsrommet 12:15-14:0012:15-14:00HHT 01.202SOK-1005-1ForelesningD.G. KidaneØ. MyrlandE. Sirnes	12:15-14:00	HHT 01.202	SOK-1005-1	Forelesning	D.G. KidaneØ. MyrlandE. Sirnes	
12:15-14:00	HHT 01.202
SOK-1005-1	Forelesning
D.G. KidaneØ. MyrlandE. Sirnes	
13:00				
14:00					
15:00					
16:00					
12:15-14:00	HHT 01.202
SOK-1005-1	Forelesning
D.G. KidaneØ. MyrlandE. Sirnes	
Uke  3	Mandag  15.01.2024	Tirsdag  16.01.2024	Onsdag  17.01.2024	Torsdag  18.01.2024	Fredag  19.01.2024
08:00					
09:00					
10:00					
11:00					
12:00		Aktiviteter i tidsrommet 12:15-14:0012:15-14:00HHT 02.119SOK-1005-1ForelesningKontortid med studentassistent SOK-1005 og SOK-1006	12:15-14:00	HHT 02.119	SOK-1005-1	ForelesningKontortid med studentassistent SOK-1005 og SOK-1006					Aktiviteter i tidsrommet 12:15-14:0012:15-14:00HHT 01.202SOK-1005-1For

Når nettsiden ikke er "vennligsinnet"

Det er ikke alle nettsideeiere som synes det er greit at vi skraper nettsidene deres. For ordens skyld så er det altså helt lovlig å skrape nettsider. Når noen legger ut data på en nettside har de offentliggjort dataene, og kan ikke bestemme hvordan dataene skal leses. Dette gjelder selv om de legger ut beskjed om noe annet.

Det som kan være ulovlig, er å videreformidle dataene.

Når nettsiden er vanskelig å skrape, er selenium av google en veldig nyttig pakke. Med den kan koden din opptre som en vanlig bruker. Så lenge du kan se dataene på skjermen din, bør det da i prinsippet være mulig å skrape enhver side.

Vi har ikke tid til å gå inn på bruken av selenium i dette kurset, men her er en kode som bruker selenium til å skrape nordpool.no. De er ikke spesielt interessert i at vi gjør det, om du forsøker å skrape flere ganger kommer det opp en advarsel om at det er ulovlig, som altså ikke medfører riktighet.

Her er imidlertid en kode som gjøre det mulig å skrape Nordpools nettsider med selenium

Oppgave

1) Finn en side på nettet, som ikke er skrapet her (eller i R-delen av kurset) og skrap den 2) Få siden inn i pandas 3) Gjør noen beregninger du synes er interessante med dataene 4) Lagre tabllen(e)